/*	man 2.5 - display online manual pages		Author: Kees J. Bot
 *								17 Mar 1993
 */
#define nil NULL
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdarg.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/wait.h>

/* Defaults: */
char MANPATH[]=	"/usr/local/man:/usr/man:/usr/gnu/man";
char PAGER[]=	"more";

/* Comment at the start to let tbl(1) be run before n/troff. */
char TBL_MAGIC[] = ".\\\"t\n";

#define arraysize(a)	(sizeof(a) / sizeof((a)[0]))
#define arraylimit(a)	((a) + arraysize(a))
#define between(a, c, z) ((unsigned) ((c) - (a)) <= (unsigned) ((z) - (a)))

/* Section 1x uses special macros under Minix. */
#if __minix
#define SEC1xSPECIAL	1
#else
#define SEC1xSPECIAL	0
#endif

int searchwhatis(FILE *wf, char *title, char **ppage, char **psection)
/* Search a whatis file for the next occurence of "title".  Return the basename
 * of the page to read and the section it is in.  Return 0 on failure, 1 on
 * success, -1 on EOF or error.
 */
{
    static char page[256], section[32];
    char alias[256];
    int found= 0;
    int c;

    /* Each whatis line should have the format:
     *	page, title, title (section) - descriptive text
     */

    /* Search the file for a line with the title. */
    do {
	int first= 1;
	char *pc= section;

	c= fgetc(wf);

	/* Search the line for the title. */
	do {
	    char *pa= alias;

	    while (c == ' ' || c == '\t' || c == ',') c= fgetc(wf);

	    while (c != ' ' && c != '\t' && c != ','
		    && c != '(' && c != '\n' && c != EOF
	    ) {
		if (pa < arraylimit(alias)-1) *pa++= c;
		c= getc(wf);
	    }
	    *pa= 0;
	    if (first) { strcpy(page, alias); first= 0; }

	    if (strcmp(alias, title) == 0) found= 1;
	} while (c != '(' && c != '\n' && c != EOF);

	if (c != '(') {
	    found= 0;
	} else {
	    while ((c= fgetc(wf)) != ')' && c != '\n' && c != EOF) {
		if ('A' <= c && c <= 'Z') c= c - 'A' + 'a';
		if (pc < arraylimit(section)-1) *pc++= c;
	    }
	    *pc= 0;
	    if (c != ')' || pc == section) found= 0;
	}
	while (c != EOF && c != '\n') c= getc(wf);
    } while (!found && c != EOF);

    if (found) {
	*ppage= page;
	*psection= section;
    }
    return c == EOF ? -1 : found;
}

int searchwindex(FILE *wf, char *title, char **ppage, char **psection)
/* Search a windex file for the next occurence of "title".  Return the basename
 * of the page to read and the section it is in.  Return 0 on failure, 1 on
 * success, -1 on EOF or error.
 */
{
    static char page[256], section[32];
    static long low, high;
    long mid0, mid1;
    int c;
    unsigned char *pt;
    char *pc;

    /* Each windex line should have the format:
     *	title page (section) - descriptive text
     * The file is sorted.
     */

    if (ftell(wf) == 0) {
	/* First read of this file, initialize. */
	low= 0;
	fseek(wf, (off_t) 0, SEEK_END);
	high= ftell(wf);
    }

    /* Binary search for the title. */
    while (low <= high) {
	pt= (unsigned char *) title;

	mid0= mid1= (low + high) >> 1;
	if (mid0 == 0) {
	    if (fseek(wf, (off_t) 0, SEEK_SET) != 0)
		return -1;
	} else {
	    if (fseek(wf, (off_t) mid0 - 1, SEEK_SET) != 0)
		return -1;

	    /* Find the start of a line. */
	    while ((c= getc(wf)) != EOF && c != '\n')
		mid1++;
	    if (ferror(wf)) return -1;
	}

	/* See if the line has the title we seek. */
	for (;;) {
	    if ((c= getc(wf)) == ' ' || c == '\t') c= 0;
	    if (c == 0 || c != *pt) break;
	    pt++;
	}

	/* Halve the search range. */
	if (c == EOF || *pt <= c) {
	    high= mid0 - 1;
	} else {
	    low= mid1 + 1;
	}
    }

    /* Look for the title from 'low' onwards. */
    if (fseek(wf, (off_t) low, SEEK_SET) != 0)
	return -1;

    do {
	if (low != 0) {
	    /* Find the start of a line. */
	    while ((c= getc(wf)) != EOF && c != '\n')
		low++;
	    if (ferror(wf)) return -1;
	}

	/* See if the line has the title we seek. */
	pt= (unsigned char *) title;

	for (;;) {
	    if ((c= getc(wf)) == EOF) return 0;
	    low++;
	    if (c == ' ' || c == '\t') c= 0;
	    if (c == 0 || c != *pt) break;
	    pt++;
	}
    } while (c < *pt);

    if (*pt != c) return 0;		/* Not found. */

    /* Get page and section. */
    while ((c= fgetc(wf)) == ' ' || c == '\t') {}

    pc= page;
    while (c != ' ' && c != '\t' && c != '(' && c != '\n' && c != EOF) {
	if (pc < arraylimit(page)-1) *pc++= c;
	c= getc(wf);
    }
    if (pc == page) return 0;
    *pc= 0;

    while (c == ' ' || c == '\t') c= fgetc(wf);

    if (c != '(') return 0;

    pc= section;
    while ((c= fgetc(wf)) != ')' && c != '\n' && c != EOF) {
	if ('A' <= c && c <= 'Z') c= c - 'A' + 'a';
	if (pc < arraylimit(section)-1) *pc++= c;
    }
    *pc= 0;
    if (c != ')' || pc == section) return 0;

    while (c != EOF && c != '\n') c= getc(wf);
    if (c != '\n') return 0;

    *ppage= page;
    *psection= section;
    return 1;
}

char ALL[]= "";		/* Magic sequence of all sections. */

int all= 0;		/* Show all pages with a given title. */
int whatis= 0;		/* man -f word == whatis word. */
int apropos= 0;		/* man -k word == apropos word. */
int quiet= 0;		/* man -q == quietly check. */
enum ROFF { NROFF, TROFF } rofftype= NROFF;
char *roff[] = { "nroff", "troff" };

int shown;		/* True if something has been shown. */
int tty;		/* True if displaying on a terminal. */
char *manpath;		/* The manual directory path. */
char *pager;		/* The pager to use. */

char *pipeline[8][8];	/* An 8 command pipeline of 7 arguments each. */
char *(*plast)[8] = pipeline;

void putinline(char *arg1, ...)
/* Add a command to the pipeline. */
{
    va_list ap;
    char **argv;

    argv= *plast++;
    *argv++= arg1;

    va_start(ap, arg1);
    while ((*argv++= va_arg(ap, char *)) != nil) {}
    va_end(ap);
}

void execute(int set_mp, char *file)
/* Execute the pipeline build with putinline().  (This is a lot of work to
 * avoid a call to system(), but it so much fun to do it right!)
 */
{
    char *(*plp)[8], **argv;
    char *mp;
    int fd0, pfd[2], err[2];
    pid_t pid;
    int r, status;
    int last;
    void (*isav)(int sig), (*qsav)(int sig), (*tsav)(int sig);

    if (tty) {
	/* Must run this through a pager. */
	putinline(pager, (char *) nil);
    }
    if (plast == pipeline) {
	/* No commands at all? */
	putinline("cat", (char *) nil);
    }

    /* Add the file as argument to the first command. */
    argv= pipeline[0];
    while (*argv != nil) argv++;
    *argv++= file;
    *argv= nil;

    /* Start the commands. */
    fd0= 0;
    for (plp= pipeline; plp < plast; plp++) {
	argv= *plp;
	last= (plp+1 == plast);

	/* Create an error pipe and pipe between this command and the next. */
	if (pipe(err) < 0 || (!last && pipe(pfd) < 0)) {
	    fprintf(stderr, "man: can't create a pipe: %s\n", strerror(errno));
	    exit(1);
	}

	(void) fcntl(err[1], F_SETFD, fcntl(err[1], F_GETFD) | FD_CLOEXEC);

	if ((pid = fork()) < 0) {
	    fprintf(stderr, "man: cannot fork: %s\n", strerror(errno));
	    exit(1);
	}
	if (pid == 0) {
	    /* Child. */
	    if (set_mp) {
		mp= malloc((8 + strlen(manpath) + 1) * sizeof(*mp));
		if (mp != nil) {
		    strcpy(mp, "MANPATH=");
		    strcat(mp, manpath);
		    (void) putenv(mp);
		}
	    }

	    if (fd0 != 0) {
		dup2(fd0, 0);
		close(fd0);
	    }
	    if (!last) {
		close(pfd[0]);
		if (pfd[1] != 1) {
		    dup2(pfd[1], 1);
		    close(pfd[1]);
		}
	    }
	    close(err[0]);
	    execvp(argv[0], argv);
	    (void) write(err[1], &errno, sizeof(errno));
	    _exit(1);
	}

	close(err[1]);
	if (read(err[0], &errno, sizeof(errno)) != 0) {
	    fprintf(stderr, "man: %s: %s\n", argv[0],
			    strerror(errno));
	    exit(1);
	}
	close(err[0]);

	if (!last) {
	    close(pfd[1]);
	    fd0= pfd[0];
	}
	set_mp= 0;
    }

    /* Wait for the last command to finish. */
    isav= signal(SIGINT, SIG_IGN);
    qsav= signal(SIGQUIT, SIG_IGN);
    tsav= signal(SIGTERM, SIG_IGN);
    while ((r= wait(&status)) != pid) {
	if (r < 0) {
	    fprintf(stderr, "man: wait(): %s\n", strerror(errno));
	    exit(1);
	}
    }
    (void) signal(SIGINT, isav);
    (void) signal(SIGQUIT, qsav);
    (void) signal(SIGTERM, tsav);
    if (status != 0) exit(1);
    plast= pipeline;
}

void keyword(char *keyword)
/* Make an apropos(1) or whatis(1) call. */
{
    putinline(apropos ? "apropos" : "whatis",
		all ? "-a" : (char *) nil,
		(char *) nil);

    if (tty) {
	printf("Looking for keyword '%s'\n", keyword);
	fflush(stdout);
    }

    execute(1, keyword);
}

enum pagetype { CAT, CATZ, MAN, MANZ, SMAN, SMANZ };

int showpage(char *page, enum pagetype ptype, char *macros)
/* Show a manual page if it exists using the proper decompression and
 * formatting tools.
 */
{
    struct stat st;

    /* We want a normal file without X bits if not a full path. */
    if (stat(page, &st) < 0) return 0;

    if (!S_ISREG(st.st_mode)) return 0;
    if ((st.st_mode & 0111) && page[0] != '/') return 0;

    /* Do we only care if it exists? */
    if (quiet) { shown= 1; return 1; }

    if (ptype == CATZ || ptype == MANZ || ptype == SMANZ) {
	putinline("zcat", (char *) nil);
    }

    if (ptype == SMAN || ptype == SMANZ) {
	/* Change SGML into regular *roff. */
	putinline("/usr/lib/sgml/sgml2roff", (char *) nil);
	putinline("tbl", (char *) nil);
	putinline("eqn", (char *) nil);
    }

    if (ptype == MAN) {
	/* Do we need tbl? */
	FILE *fp;
	int c;
	char *tp = TBL_MAGIC;

	if ((fp = fopen(page, "r")) == nil) {
	    fprintf(stderr, "man: %s: %s\n", page, strerror(errno));
	    exit(1);
	}
	c= fgetc(fp);
	for (;;) {
	    if (c == *tp || (c == '\'' && *tp == '.')) {
		if (*++tp == 0) {
		    /* A match, add tbl. */
		    putinline("tbl", (char *) nil);
		    break;
		}
	    } else {
		/* No match. */
		break;
	    }
	    while ((c = fgetc(fp)) == ' ' || c == '\t') {}
	}
	fclose(fp);
    }

    if (ptype == MAN || ptype == MANZ || ptype == SMAN || ptype == SMANZ) {
	putinline(roff[rofftype], macros, (char *) nil);
    }

    if (tty) {
	printf("%s %s\n",
	    ptype == CAT || ptype == CATZ ? "Showing" : "Formatting", page);
	fflush(stdout);
    }
    execute(0, page);

    shown= 1;
    return 1;
}

int member(char *word, char *list)
/* True if word is a member of a comma separated list. */
{
    size_t len= strlen(word);

    if (list == ALL) return 1;

    while (*list != 0) {
	if (strncmp(word, list, len) == 0
		&& (list[len] == 0 || list[len] == ','))
	    return 1;
	while (*list != 0 && *list != ',') list++;
	if (*list == ',') list++;
    }
    return 0;
}

int trymandir(char *mandir, char *title, char *section)
/* Search the whatis file of the manual directory for a page of the given
 * section and display it.
 */
{
    FILE *wf;
    char whatis[1024], pagename[1024], *wpage, *wsection;
    int rsw, rsp;
    int ntries;
    int (*searchidx)(FILE *, char *, char **, char **);
    struct searchnames {
	enum pagetype	ptype;
	char		*pathfmt;
    } *sp;
    static struct searchnames searchN[] = {
	{ CAT,	"%s/cat%s/%s.%s"	},	/* SysV */
	{ CATZ,	"%s/cat%s/%s.%s.Z"	},
	{ MAN,	"%s/man%s/%s.%s"	},
	{ MANZ,	"%s/man%s/%s.%s.Z"	},
	{ SMAN,	"%s/sman%s/%s.%s"	},	/* Solaris */
	{ SMANZ,"%s/sman%s/%s.%s.Z"	},
	{ CAT,	"%s/cat%.1s/%s.%s"	},	/* BSD */
	{ CATZ,	"%s/cat%.1s/%s.%s.Z"	},
	{ MAN,	"%s/man%.1s/%s.%s"	},
	{ MANZ,	"%s/man%.1s/%s.%s.Z"	},
    };

    if (strlen(mandir) + 1 + 6 + 1 > arraysize(whatis)) return 0;

    /* Prefer a fast windex database if available. */
    sprintf(whatis, "%s/windex", mandir);

    if ((wf= fopen(whatis, "r")) != nil) {
	searchidx= searchwindex;
    } else {
	/* Use a classic whatis database. */
	sprintf(whatis, "%s/whatis", mandir);

	if ((wf= fopen(whatis, "r")) == nil) return 0;
	searchidx= searchwhatis;
    }

    rsp= 0;
    while (!rsp && (rsw= (*searchidx)(wf, title, &wpage, &wsection)) == 1) {
	if (!member(wsection, section)) continue;

	/* When looking for getc(1S) we try:
	 *	cat1s/getc.1s
	 *	cat1s/getc.1s.Z
	 *	man1s/getc.1s
	 *	man1s/getc.1s.Z
	 *	sman1s/getc.1s
	 *	sman1s/getc.1s.Z
	 *	cat1/getc.1s
	 *	cat1/getc.1s.Z
	 *	man1/getc.1s
	 *	man1/getc.1s.Z
	 */

	if (strlen(mandir) + 2 * strlen(wsection) + strlen(wpage)
		    + 10 > arraysize(pagename))
	    continue;

	sp= searchN;
	ntries= arraysize(searchN);
	do {
	    if (sp->ptype <= CATZ && rofftype != NROFF)
		continue;

	    sprintf(pagename, sp->pathfmt,
		mandir, wsection, wpage, wsection);

	    rsp= showpage(pagename, sp->ptype,
		(SEC1xSPECIAL && strcmp(wsection, "1x") == 0) ? "-mnx" : "-man");
	} while (sp++, !rsp && --ntries != 0);

	if (all) rsp= 0;
    }
    if (rsw < 0 && ferror(wf)) {
	fprintf(stderr, "man: %s: %s\n", whatis, strerror(errno));
	exit(1);
    }
    fclose(wf);
    return rsp;
}

int trysubmandir(char *mandir, char *title, char *section)
/* Search the subdirectories of this manual directory for whatis files, they
 * may have manual pages that override the ones in the major directory.
 */
{
    char submandir[1024];
    DIR *md;
    struct dirent *entry;

    if ((md= opendir(mandir)) == nil) return 0;

    while ((entry= readdir(md)) != nil) {
	if (strcmp(entry->d_name, ".") == 0
	    || strcmp(entry->d_name, "..") == 0) continue;
	if ((strncmp(entry->d_name, "man", 3) == 0
	    || strncmp(entry->d_name, "cat", 3) == 0)
	    && between('0', entry->d_name[3], '9')) continue;

	if (strlen(mandir) + 1 + strlen(entry->d_name) + 1
		    > arraysize(submandir)) continue;

	sprintf(submandir, "%s/%s", mandir, entry->d_name);

	if (trymandir(submandir, title, section) && !all) {
	    closedir(md);
	    return 1;
	}
    }
    closedir(md);

    return 0;
}

void searchmanpath(char *title, char *section)
/* Search the manual path for a manual page describing "title." */
{
    char mandir[1024];
    char *pp= manpath, *pd;

    for (;;) {
	while (*pp != 0 && *pp == ':') pp++;

	if (*pp == 0) break;

	pd= mandir;
	while (*pp != 0 && *pp != ':') {
	    if (pd < arraylimit(mandir)) *pd++= *pp;
	    pp++;
	}
	if (pd == arraylimit(mandir)) continue;		/* forget it */

	*pd= 0;
	if (trysubmandir(mandir, title, section) && !all) break;
	if (trymandir(mandir, title, section) && !all) break;
    }
}

void usage(void)
{
    fprintf(stderr, "Usage: man -[antfkq] [-M path] [-s section] title ...\n");
    exit(1);
}

int main(int argc, char **argv)
{
    char *title, *section= ALL;
    int i;
    int nomoreopt= 0;
    char *opt;

    if ((pager= getenv("PAGER")) == nil) pager= PAGER;
    if ((manpath= getenv("MANPATH")) == nil) manpath= MANPATH;
    tty= isatty(1);

    i= 1;
    do {
	while (i < argc && argv[i][0] == '-' && !nomoreopt) {
	    opt= argv[i++]+1;
	    if (opt[0] == '-' && opt[1] == 0) {
		nomoreopt= 1;
		break;
	    }
	    while (*opt != 0) {
		switch (*opt++) {
		case 'a':
		    all= 1;
		    break;
		case 'f':
		    whatis= 1;
		    break;
		case 'k':
		    apropos= 1;
		    break;
		case 'q':
		    quiet= 1;
		    break;
		case 'n':
		    rofftype= NROFF;
		    apropos= whatis= 0;
		    break;
		case 't':
		    rofftype= TROFF;
		    apropos= whatis= 0;
		    break;
		case 's':
		    if (*opt == 0) {
			if (i == argc) usage();
			section= argv[i++];
		    } else {
			section= opt;
			opt= "";
		    }
		    break;
		case 'M':
		    if (*opt == 0) {
			if (i == argc) usage();
			manpath= argv[i++];
		    } else {
			manpath= opt;
			opt= "";
		    }
		    break;
		default:
		    usage();
		}
	    }
	}

	if (i >= argc) usage();

	if (between('0', argv[i][0], '9') && i+1 < argc) {
	    /* Old BSD style section designation? */
	    section= argv[i++];
	}
	if (i == argc) usage();

	title= argv[i++];

	if (whatis || apropos) {
	    keyword(title);
	} else {
	    shown= 0;
	    searchmanpath(title, section);

	    if (!shown) (void) showpage(title, MAN, "-man");

	    if (!shown) {
		if (!quiet) {
		    fprintf(stderr,
			"man: no manual on %s\n",
			title);
		}
		exit(1);
	    }
	}
    } while (i < argc);

    return 0;
}
